/**
 * 
 */
package org.stars.util.reflect;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.io.Writer;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.util.Collection;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.logging.Logger;

import javax.servlet.Filter;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletResponse;

import org.stars.datastructure.map.CaseInsensitiveLinkedHashMap;
import org.stars.datastructure.stack.Stack;

/**
 * Classe di utilità per la reflection.
 * 
 * @author Francesco Benincasa (908099)
 * 
 */
public abstract class ReflectUtil {

	/**
	 * Dato un oggetto, converte tutti i suoi attributi come valori di una
	 * mappa, le cui coppie di chiave valore e' dato dal nome del campo e dal
	 * relativo tipo. Nell'insieme <i>ignoredAttributes</i> e' possibile
	 * definire un set di attributi da ignorare.
	 * 
	 * @param object
	 *            oggetto da analizzare
	 * @return mappa di {nome field, valore}
	 */
	public static Map<String, Class<?>> getFieldTypesAsMap(Object object) {
		return getFieldTypesAsMap(object, null);
	}
	
	/**
	 * Dato un oggetto, converte tutti i suoi attributi come valori di una
	 * mappa, le cui coppie di chiave valore e' dato dal nome del campo e dal
	 * relativo valore. Nell'insieme <i>ignoredAttributes</i> e' possibile
	 * definire un set di attributi da ignorare.
	 * 
	 * @param object
	 * @param ignoredAttributes
	 * @return
	 */
	public static Map<String, Class<?>> getFieldTypesAsMap(Object object, Set<String> ignoredAttributes) {
		if (ignoredAttributes == null) {
			ignoredAttributes = new TreeSet<String>();
		}

		String fieldName;
		Map<String, Class<?>> map = new CaseInsensitiveLinkedHashMap<Class<?>>();

		// se object null usciamo subito
		if (object == null)
			return map;

		Field[] fields;
		Class<?> currentClass = object.getClass();

		do {
			fields = currentClass.getDeclaredFields();

			for (Field item : fields) {
				try {
					fieldName = item.getName();					

					if (!ignoredAttributes.contains(fieldName.toLowerCase())) {
						map.put(fieldName, item.getType());
					}

				} catch (Exception e) {

				} finally {
					item.setAccessible(false);
				}
			}

			currentClass = currentClass.getSuperclass();
		} while (!currentClass.equals(Object.class));

		return map;
	}

	/**
	 * Dato un oggetto, converte tutti i suoi attributi come valori di una
	 * mappa, le cui coppie di chiave valore e' dato dal nome del campo e dal
	 * relativo valore. Nell'insieme <i>ignoredAttributes</i> e' possibile
	 * definire un set di attributi da ignorare.
	 * 
	 * @param object
	 *            oggetto da analizzare
	 * @return mappa di {nome field, valore}
	 */
	public static Map<String, Object> getFieldsAsMap(Object object) {
		return getFieldsAsMap(object, null);
	}

	/**
	 * Dato un oggetto, converte tutti i suoi attributi come valori di una
	 * mappa, le cui coppie di chiave valore e' dato dal nome del campo e dal
	 * relativo valore. Nell'insieme <i>ignoredAttributes</i> e' possibile
	 * definire un set di attributi da ignorare.
	 * 
	 * @param object
	 *            oggetto da analizzare
	 * @param ignoredAttributes
	 *            set di attributi da ignorare
	 * @return mappa di {nome field, valore}
	 */
	public static Map<String, Object> getFieldsAsMap(Object object, Set<String> ignoredAttributes) {
		if (ignoredAttributes == null) {
			ignoredAttributes = new TreeSet<String>();
		}

		String fieldName;
		Map<String, Object> map = new CaseInsensitiveLinkedHashMap<Object>();

		// se object null usciamo subito
		if (object == null)
			return map;

		Object fieldValue = null;

		Field[] fields;
		Class<?> currentClass = object.getClass();

		do {
			fields = currentClass.getDeclaredFields();

			for (Field item : fields) {
				try {
					fieldName = item.getName();

					if (!ignoredAttributes.contains(fieldName.toLowerCase())) {

						item.setAccessible(true);

						fieldValue = item.get(object);
						map.put(fieldName, fieldValue);
					}

				} catch (Exception e) {

				} finally {
					item.setAccessible(false);
				}
			}

			currentClass = currentClass.getSuperclass();
		} while (!currentClass.equals(Object.class));

		return map;
	}
	

	/**
	 * <p>
	 * Recupera l'elenco degli attributi non nulli che possono essere visti come
	 * <code>type</code>. Vengono analizzate anche le classi padre, fino alla
	 * classe Object, esclusa.
	 * </p>
	 * <p>
	 * Nel seguente esempio, viene illustrato come recuperare da un oggetto di
	 * tipo <code>BlPersona</code>, tutti i campi non nulli che implementano
	 * l'interfaccia <code>DaoSessionWorker</code>.
	 * </p>
	 * 
	 * <pre>
	 * BlPersona bl = new BlPersona();
	 * List&lt;Field&gt; lista = ReflectUtil.getFieldsAsType(DaoSessionWorker.class, bl);
	 * for (Field item : lista) {
	 * 	System.out.println(item.getName());
	 * }
	 * </pre>
	 * 
	 * @param type
	 * @param object
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public static <T> List<T> getFieldsAsType(Class<T> type, Object object) {
		List<T> lista = new LinkedList<T>();

		// se object null usciamo subito
		if (object == null || type == null)
			return lista;

		Object tempItem = null;

		Field[] fields;
		Class<?> currentClass = object.getClass();
		Class<?> currentItemClass;

		do {
			fields = currentClass.getDeclaredFields();

			for (Field item : fields) {
				try {
					item.setAccessible(true);
					tempItem = item.get(object);

					if (tempItem == null)
						continue;

					// ASSERT: object not ull
					currentItemClass = tempItem.getClass();

					if (type.isAssignableFrom(currentItemClass)) {
						lista.add((T) tempItem);
					}
				} catch (Exception e) {

				} finally {
					item.setAccessible(false);
				}
			}

			currentClass = currentClass.getSuperclass();
		} while (!currentClass.equals(Object.class));

		return lista;
	}

	/**
	 * Dato un object, restituisce solo il nome della classe senza package. Nel
	 * caso in cui l'oggetto sia null restutisce una stringa vuota. Nel caso non
	 * sia possibile ricavare per qualche motivo il nome della classe,
	 * restituisce il nome della classe intero.
	 * 
	 * @param obj
	 * @return nome della classe senza package
	 */
	public static String getOnlyClassName(Object obj) {
		String ret;

		if (obj == null)
			return "";

		ret = obj.getClass().getName();

		int i = ret.lastIndexOf(".");

		if (i > -1) {
			ret = ret.substring(i + 1);
		}

		return ret;
	}



	/**
	 * Crea una "deep copy" dell'oggetto usando la proprietà di serializzazione
	 * e deserializzazione dell'oggetto .... <h2>Prerequisito</h2> L'oggetto e
	 * tutti le sue property devono implemntare (ed essere) Serializable
	 * 
	 * @param <E>
	 *            tipo di entity bean da copiare
	 * @param ebSource
	 *            entity bean da duplicare
	 * @return copia dell'entity bean sorgente
	 * @throws Exception
	 *             in caso di errore.
	 */
	@SuppressWarnings("unchecked")
	public static <E> E createCopy(E ebSource) throws Exception {

		ObjectOutputStream oos = null;
		ObjectInputStream ois = null;

		try {
			ByteArrayOutputStream bos = new ByteArrayOutputStream();
			oos = new ObjectOutputStream(bos);
			// serialize and pass the object
			oos.writeObject(ebSource);
			oos.flush();
			ByteArrayInputStream bin = new ByteArrayInputStream(bos.toByteArray());
			ois = new ObjectInputStream(bin);
			// return the new object
			return (E) ois.readObject();

		} catch (Exception e) {
			throw (e); // in caso di errore i due non sono uguali
		} finally {
			if (oos != null)
				oos.close();
			if (ois != null)
				ois.close();
		}
	}

	/**
	 * Returns a PropertyDescriptor[] for the given Class.
	 * 
	 * @param <T>
	 * 
	 * @param c
	 *            The Class to retrieve PropertyDescriptors for.
	 * @return A PropertyDescriptor[] describing the Class.
	 * @throws Exception
	 *             if introspection failed.
	 */
	/*
	 * @Deprecated public static <T> PropertyDescriptor[]
	 * propertyDescriptors(Class<T> c) throws Exception { // Introspector caches
	 * BeanInfo classes for better performance BeanInfo beanInfo;
	 * 
	 * try { beanInfo = Introspector.getBeanInfo(c);
	 * 
	 * } catch (IntrospectionException e) { Exception ex = new
	 * Exception("Bean introspection failed: " + e.getMessage()); throw ex; }
	 * 
	 * return beanInfo.getPropertyDescriptors(); }
	 */







	/**
	 * Restituisce il nome della classe dell'oggetto passato come argomento
	 * senza i package. Nel caso l'oggetto passato è <code>null</code>, viene
	 * restuita la stringa <code>[null]</code>.
	 * 
	 * @param value
	 *            ogetto
	 * @return restituisce
	 */
	public static String getSimpleClassName(Object value) {
		String ret = "[null]";

		if (value == null)
			return ret;

		int pos;
		ret = value.getClass().toString();
		pos = ret.lastIndexOf(".");

		if (pos > -1)
			ret = ret.substring(pos + 1);

		return ret;
	}
	
	/**
	 * Dato un oggetto, immette nello <code>StringBuffer</code> i relativi
	 * attributi e i valori che essi hanno
	 * 
	 * @param obj
	 *            istanza di oggetto da ispezionare
	 * @return string con la definizione degli attributi
	 */
	public static String inspectObject(Object obj) {
		try {
			return inspectObject(obj, obj.getClass(), new StringBuilder(), new Stack<Integer>(), false).toString();
		} catch (Exception e) {
			return "[Error during parsing of object]";
		}
	}

	/**
	 * Dato un oggetto, immette nello <code>StringBuffer</code> i relativi
	 * attributi e i valori che essi hanno, incluse le collection
	 * 
	 * @param obj
	 *            istanza di oggetto da ispezionare
	 * @param bIncludeCollection
	 *            indica se includere o meno, le informazioni relative alle
	 *            varie collezioni (List, etc etc) utilizzati.
	 * @return string con la definizione degli attributi
	 */
	public static String inspectObject(Object obj, boolean bIncludeCollection) {
		try {
			return inspectObject(obj, obj.getClass(), new StringBuilder(), new Stack<Integer>(), bIncludeCollection).toString();
		} catch (Exception e) {
			return "[Error during parsing of object]";
		}
	}



	/**
	 * Dato un oggetto, immette nello <code>StringBuffer</code> i relativi
	 * attributi e i valori che essi hanno
	 * 
	 * @param obj
	 *            istanza di oggetto da ispezionare
	 * @param buffer
	 *            string buffer da ispezionare
	 * @return string buffer con la definizione degli attributi
	 */
	public static StringBuilder inspectObject(Object obj, StringBuilder buffer) {
		try {
			return inspectObject(obj, obj.getClass(), buffer, new Stack<Integer>(), false);
		} catch (Exception e) {
			return buffer.append("[Error during parsing of object]");
		}
	}

	/**
	 * Restituisce l'elenco degli attributi di un oggetto ed i relativi valori
	 * 
	 * @param obj
	 *            istanza di oggetto da ispezionare
	 * @param clazz
	 *            verranno visualizzati tutti gli attributi dell'oggetto che
	 *            fanno parte della definizione di questa classe
	 * @param buffer
	 *            string buffer nel quale inserire gli attributi
	 * @return string buffer con la definizione degli attributi
	 * @throws Exception
	 */
	private final static StringBuilder inspectObject(Object obj, Class<?> clazz, StringBuilder buffer, Stack<Integer> stack, boolean bIncludeCollection) throws Exception {

		Object objApp = "{Undefined}";
		boolean bAlreadyInStack;

		stack.push(obj.hashCode());

		if (clazz != null && buffer != null) {

			Map<String, Object> mapField=getFieldsAsMap(obj);
			
			for (Map.Entry<String, Object> item:mapField.entrySet())
			{
				buffer.append(item.getKey());
				buffer.append(" = ");
				
				if (objApp == null) {
					objApp = "{Null value}";
					buffer.append(", ");
					continue;
				}
				
				try {
					bAlreadyInStack = (stack.search(objApp.hashCode())) > -1;
				} catch (Exception e) {
					// in caso di errore blocco
					objApp = "{Undefined value (hashCode is invalid)}";
					buffer.append(", ");
					continue;
				}
				
				// classi da scartare mediante lista di classi non presenti.
				if (IgnoredClasses.list != null) {
					boolean bFound = false;
					for (String className : IgnoredClasses.list) {
						if (objApp.getClass().getName().equalsIgnoreCase(className)) {
							buffer.append("{The type " + objApp.getClass() + " is ignored}");
							buffer.append(", ");
							bFound = true;
							break;
						}
					}

					if (bFound) {
						continue;
					}
				}
				
				// Questi elenchi sono fissi ed immutabili.
				if (objApp instanceof OutputStream || objApp instanceof Logger || objApp instanceof InputStream || objApp instanceof Reader || objApp instanceof Writer
						|| objApp instanceof HttpServletResponse || objApp instanceof Filter || objApp instanceof HttpServlet) {
					buffer.append("{The type " + objApp.getClass() + " is ignored}");
					buffer.append(", ");
					continue;
				}

				if (objApp instanceof Collection) {
					Collection<?> app = (Collection<?>) objApp;

					if (app.size() > 10) {
						buffer.append("{" + "Size of collection is " + app.size() + "(Too big for display)}");
						buffer.append(", ");
						continue;
					}
				}

				if (objApp instanceof String || objApp instanceof Integer || objApp instanceof Long || objApp instanceof Double || objApp instanceof Byte
						|| objApp instanceof Character || objApp instanceof BigDecimal || objApp instanceof Boolean || objApp instanceof Float || objApp instanceof Class
						|| objApp instanceof Date) {
					buffer.append(objApp.toString());
				} else if (bAlreadyInStack) {
					buffer.append("{It is already parsed}");
				} else {
					buffer.append("{" + inspectObject(objApp, objApp.getClass(), new StringBuilder(), stack, bIncludeCollection) + "}");

				}

				buffer.append(", ");
								
			}						
		}

		if (buffer.length() > 2) {
			buffer.setLength(buffer.length() - 2);
		}

		stack.pop();

		return buffer;
	}

}
